C++ 禁止拷贝与移动的常见原因
C++ 里常把类的拷贝构造/拷贝赋值与移动构造/移动赋值用 = delete 删除。什么时候应该这样做、为什么——本文汇总六类常见原因与实现方式,并澄清一个容易误判的条目(不可变对象并不是删除拷贝的理由)。适合在类设计时对照检查。
常见原因
1. 资源管理的唯一性
- 场景:类管理独占资源(如文件句柄、网络连接、锁、硬件设备等)。
- 原因:
- 拷贝或移动可能导致资源被多个对象共享,引发重复释放或竞争条件。
- 例如,
std::unique_ptr禁止拷贝,因为指针所有权必须唯一。
2. 单例模式(Singleton)
- 场景:确保全局只有一个实例的类。
- 原因:
- 拷贝或移动会破坏单例的唯一性。
- 通常通过删除拷贝和移动操作来强制实现单例。
3. 性能优化
- 场景:类的拷贝或移动成本极高(如大型缓存、复杂状态)。
- 原因:
- 禁止拷贝/移动可以强制用户显式管理对象生命周期(如通过引用或指针传递)。
- 避免隐式拷贝导致的性能问题。
4. 线程安全性
- 场景:类包含共享状态或非线程安全的成员。
- 原因:
- 拷贝或移动可能导致多线程环境下状态不一致。
- 典型例子:含
std::mutex、std::condition_variable等同步原语成员的类——这些成员本身不可拷贝、不可移动(cppreference:std::mutex is neither copyable nor movable),包含它们的类也会隐式失去拷贝与移动能力。
5. 接口设计约束
- 场景:抽象基类或接口类。
- 原因:
- 禁止拷贝/移动可以强制子类实现自己的语义(如深拷贝)。
- 避免切片问题(slicing problem)。
6. 避免逻辑错误
- 场景:类的拷贝或移动无意义或可能导致逻辑错误。
- 原因:
- 例如,一个表示「数据库连接」的类,拷贝会导致多个对象共享同一连接,可能引发事务冲突。
容易误判的一点:不可变对象并不是删除拷贝的理由。不可变值对象通常允许拷贝(拷贝不改变唯一性),不可变语义由 const 成员保证;真正需要删除拷贝的是单例式的全局唯一状态,已由第 2 条覆盖。
实现方式
cpp
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 禁止移动(C++11 起)
NonCopyable(NonCopyable&&) = delete;
NonCopyable& operator=(NonCopyable&&) = delete;
}; 使用宏把这套删除集中定义,供多个类复用:
cpp
// 删除拷贝构造与拷贝赋值
#define DISABLE_COPY(ClassName) \
ClassName(const ClassName&) = delete; \
ClassName& operator=(const ClassName&) = delete;
// 删除移动构造与移动赋值
#define DISABLE_MOVE(ClassName) \
ClassName(ClassName&&) = delete; \
ClassName& operator=(ClassName&&) = delete;
// 删除拷贝/移动构造与拷贝/移动赋值
#define DISABLE_COPY_MOVE(ClassName) \
DISABLE_COPY(ClassName) \
DISABLE_MOVE(ClassName) 注意移动构造/移动赋值的参数应写 ClassName&&(非 const 右值引用)。写成 const ClassName&& 虽然按标准也算移动构造函数(能拦住移动),但 const 右值引用属于反惯用法,一般视为笔误。
顺带一提:只声明拷贝操作为 = delete 时,移动操作不会被隐式生成,但显式把四件套全部删除是更明确、不依赖隐式规则的写法。
小结
六类原因可以归纳为三条主线:资源与状态的唯一性(独占资源、单例、线程安全成员)、设计约束(接口类防切片)、性能与逻辑(避免天价拷贝、避免无意义复制)。C++11 的移动语义背景下,「删除拷贝但保留移动」表达独占可转移,「四件套全删」表达完全不可复制——按语义选择即可。相关背景见 C++11 新特性(右值引用与移动语义),系列总览见 现代 C++ 新特性汇总。